"""
>>> import os
>>> from google.appengine.api import apiproxy_stub_map, datastore_file_stub
>>> app_id = "edenpunch"
>>> os.environ['APPLICATION_ID'] = app_id
>>> os.environ['AUTH_DOMAIN'] = 'gmail.com'
>>> datastore_file = '/dev/null'
>>> apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
>>> stub = datastore_file_stub.DatastoreFileStub(app_id, datastore_file, '/')
>>> apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub)
"""

from google.appengine.ext import db
from google.appengine.api import users
import datetime_translator

import datetime

MAX_FETCH = 500
NO_CLOCKOUT_DATETIME = datetime.datetime(1900, 1, 1)


# Entity Tables
class Project(db.Model):
    """Models a Project"""
    name = db.StringProperty()
    user = db.UserProperty()
    archived = db.BooleanProperty(default=False)


class Punch(db.Model):
    """Models an individual Punch"""
    user = db.UserProperty()
    in_out_datetimes = db.ListProperty(datetime.datetime)
    project = db.ReferenceProperty(reference_class=Project, collection_name="punches")


class Workweek(db.Model):
    """Models a work week"""
    start_day = db.StringProperty()
    end_day = db.StringProperty()

# model methods

def get_workweek():
    workweek = Workweek.all().get()
    if not workweek:
        # default workweek
        workweek = Workweek(start_day="Saturday", end_day="Friday")
    return workweek


def is_clocked_in():
    result = Punch.all().filter("in_out_datetimes =", NO_CLOCKOUT_DATETIME).filter("user =",
        users.get_current_user()).fetch(1)
    if len(result) == 1:
        return result[0].key(), result[0].project.name
    else:
        return ""


def get_projects(all=False, get_archived=True):
    clocked_in = is_clocked_in()
    if clocked_in and not all:
        key = clocked_in[0]
        return [get_punch(key).project]
    else:
        if get_archived:
            return Project.all().filter("user =", users.get_current_user()).order("name").fetch(MAX_FETCH)
        else:
            return Project.all().filter("user =", users.get_current_user()).filter("archived =", False).order(
                "name").fetch(MAX_FETCH)


def get_project_names(all=False):
    return [p.name for p in get_projects(all)]


def get_punch(key):
    punch = Punch.get(key)
    return punch


def get_project(key):
    project = Project.get(key)
    return project


def get_project_sums(start_datetime, end_datetime, all=False):
    """
    Returns an iterable object with project and sum
    as values in each item in the object
    """
    sums = []
    grand_sum = 0.0
    for project_name in get_project_names(all=True):
        sum = get_sum_for_project_by_datetime(project_name, start_datetime,
            end_datetime)
        if float(sum) != 0.0 or all:
            sums.append({"project": project_name,
                         "sum": sum})
            grand_sum += float(sum)
    return sums, grand_sum


def _convert_timedelta_to_hours(td_obj):
    days = td_obj.days
    seconds = td_obj.seconds
    hours = days * 24
    hours += (seconds / 60.0 / 60.0)
    return hours


def get_hours_from_punch(punch):
    if punch.in_out_datetimes[1] == NO_CLOCKOUT_DATETIME:
        out_datetime = datetime.datetime.now()
    else:
        out_datetime = punch.in_out_datetimes[1]
    punch_delta = out_datetime - punch.in_out_datetimes[0]
    hours = _convert_timedelta_to_hours(punch_delta)
    return hours


def get_sum_for_project_by_datetime(project_name, local_daystart_datetime,
                                    dayafter_daystart_datetime):
    project_sum = 0.0
    punches = get_punches_for_date_range(local_daystart_datetime, dayafter_daystart_datetime)
    for punch in punches:
        if punch.project.name == project_name:
            project_sum += get_hours_from_punch(punch)
    project_sum = "%05.2f" % project_sum
    return project_sum


def get_todays_punches(local_daystart_datetime, dayafter_daystart_datetime):
    punches = get_punches_for_date_range(local_daystart_datetime, dayafter_daystart_datetime)
    return punches


def get_sum_for_date_range(day_start, next_day_start_after_end):
    punches = get_punches_for_date_range(day_start, next_day_start_after_end)
    sum = 0.0
    for punch in punches:
        sum += get_hours_from_punch(punch)
    return sum


def get_punches_for_date_range(day_start, next_day_start):
    punches = Punch.all()
    punches.filter("in_out_datetimes >=", day_start)
    punches.filter("in_out_datetimes <", next_day_start)
    punches.filter("user =", users.get_current_user())
    punches.order("-in_out_datetimes")
    return punches.fetch(MAX_FETCH)


def _convert_dow_str_to_num(dow_str):
    if dow_str == "Monday":
        return 0
    elif dow_str == "Tuesday":
        return 1
    elif dow_str == "Wednesday":
        return 2
    elif dow_str == "Thursday":
        return 3
    elif dow_str == "Friday":
        return 4
    elif dow_str == "Saturday":
        return 5
    elif dow_str == "Sunday":
        return 6
    else:
        raise Exception("Invalid day of week string '%s'" % dow_str)


def get_midnight_dt(days_diff, dt):
    """
    @param days_diff: number of days different from dt
    @type  days_diff: int
    @param dt: date to start with
    @type  dt: datetime.date

    Returns a datetime object (at midnight)
    that equates to dt + days_diff
    """
    start_dt_midnight = datetime.datetime.combine(dt + datetime.timedelta(days=days_diff),
        datetime.time())
    return datetime_translator.convert_to_UTC(start_dt_midnight)


def get_dates_for_workweek(today_midnight):
    """
    Returns the start day and day after the last day at midnight
    according to the workweek

    @param today_midnight: today at midnight
    @type today_midnight: datetime.datetime

    @returns (start of workweek datetime, day after end of workweek at midnight)
    @rtype tuple

    012345601234560123456
         |       |

    >>> dates = get_dates_for_workweek(datetime.datetime.combine(datetime.date(2012, 02, 13), datetime.time()))
    >>> dates[0] == datetime_translator.convert_to_UTC(datetime.datetime(2012, 2, 11, 0, 0))
    True
    >>> dates[1] == datetime_translator.convert_to_UTC(datetime.datetime(2012, 2, 18, 0, 0))
    True
    """
    workweek = get_workweek()
    day_of_week = today_midnight.weekday()
    workweek_start = _convert_dow_str_to_num(workweek.start_day)
    days_before = workweek_start - day_of_week
    if workweek_start >= day_of_week:
        days_before -= 7
    start_dt_midnight = get_midnight_dt(days_before, today_midnight)
    day_after_end_dt_midnight = get_midnight_dt(7, start_dt_midnight)
    return start_dt_midnight, day_after_end_dt_midnight


def punches_in_project(project_key):
    project = get_project(project_key)
    count = Punch.all().filter("project =", project).filter("user =", users.get_current_user()).count(1)
    return count > 0


def _get_project(project_name):
    """Get an already existing project from it's name"""
    projects = Project.all().filter("name =", project_name).filter("user =", users.get_current_user()).fetch(1)
    return projects[0]


def save_punch(project_key, in_utc_datetime=None, out_utc_datetime=NO_CLOCKOUT_DATETIME,
               punch_key=None):
    """creates or edits a punch entity"""
    if not punch_key:
        punch = Punch()
    else:
        punch = get_punch(punch_key)
        # clear out the datetimes so we can redo them here
    punch.in_out_datetimes = []
    # we are assuming that the project already exists
    punch.project = get_project(project_key)
    punch.in_out_datetimes.append(in_utc_datetime)
    punch.in_out_datetimes.append(out_utc_datetime)
    punch.user = users.get_current_user()
    punch.put()


def delete_punch(key):
    punch = get_punch(key)
    punch.delete()


def save_project(project_name, punch_key=None, archived=False):
    if not punch_key:
        project = Project(name=project_name, archived=archived)
    else:
        project = get_project(punch_key)
        project.name = project_name
        project.archived = archived
    project.user = users.get_current_user()
    project.put()


def delete_project(key):
    project = get_project(key)
    project.delete()


def delete_workweek():
    workweek = get_workweek()
    if workweek:
        workweek.delete()


def save_workweek(start_day, end_day):
    delete_workweek()
    workweek = Workweek(start_day=start_day, end_day=end_day)
    workweek.put()


def get_workweek_hours(today_midnight):
    """
    >>> get_workweek_hours(datetime_translator.get_today_daystart_and_tomorrow_in_utc()[0])
    0.0
    """
    start_datetime_utc, end_datetime_utc = get_dates_for_workweek(today_midnight)
    return get_sum_for_date_range(start_datetime_utc, end_datetime_utc)

if __name__ == "__main__":
    import doctest

    doctest.testmod(verbose=True)
